From e3164246e1733c717072b804566a485458769314 Mon Sep 17 00:00:00 2001
From: Ilion Beyst <ilion.beyst@gmail.com>
Date: Mon, 24 Oct 2022 07:19:52 +0200
Subject: create simple stats page

---
 .../src/routes/bots/[bot_name]/stats.svelte        | 169 +++++++++++++++++++++
 1 file changed, 169 insertions(+)
 create mode 100644 web/pw-server/src/routes/bots/[bot_name]/stats.svelte

(limited to 'web/pw-server/src/routes/bots/[bot_name]/stats.svelte')

diff --git a/web/pw-server/src/routes/bots/[bot_name]/stats.svelte b/web/pw-server/src/routes/bots/[bot_name]/stats.svelte
new file mode 100644
index 0000000..6b5a2e1
--- /dev/null
+++ b/web/pw-server/src/routes/bots/[bot_name]/stats.svelte
@@ -0,0 +1,169 @@
+<script lang="ts" context="module">
+  import { ApiClient } from "$lib/api_client";
+
+  export async function load({ params, fetch }) {
+    const apiClient = new ApiClient(fetch);
+
+    try {
+      const bot_name = params["bot_name"];
+      const [botData, botStats, leaderboard] = await Promise.all([
+        apiClient.get(`/api/bots/${bot_name}`),
+        apiClient.get(`/api/bots/${bot_name}/stats`),
+        apiClient.get("/api/leaderboard"),
+      ]);
+
+      const { bot, owner } = botData;
+      return {
+        props: {
+          bot,
+          owner,
+          botStats,
+          leaderboard,
+        },
+      };
+    } catch (error) {
+      return {
+        status: error.status,
+        error: error,
+      };
+    }
+  }
+
+  function mergedStats(rawStats: object) {
+    return Object.fromEntries(
+      Object.entries(rawStats).map(([opponent, ms]) => {
+        const mapStats = ms as { k: MatchupStats };
+        return [opponent, Object.values(mapStats).reduce(mergeStats)];
+      })
+    );
+  }
+
+  type MatchupStats = {
+    win: number;
+    tie: number;
+    loss: number;
+  };
+
+  function winRate(stats: MatchupStats) {
+    return (stats.win + 0.5 * stats.tie) / (stats.win + stats.tie + stats.loss);
+  }
+
+  function mergeStats(a: MatchupStats, b: MatchupStats): MatchupStats {
+    return {
+      win: a.win + b.win,
+      tie: a.tie + b.tie,
+      loss: a.loss + b.loss,
+    };
+  }
+</script>
+
+<script lang="ts">
+  export let bot: object;
+  export let owner: object;
+  export let botStats: object;
+  export let leaderboard: object[];
+
+  $: mergedStats = mergedStats(botStats);
+</script>
+
+<div class="container">
+  <div class="header">
+    <h1 class="bot-name">{bot["name"]}</h1>
+    {#if owner}
+      <a class="owner-name" href="/users/{owner['username']}">
+        {owner["username"]}
+      </a>
+    {/if}
+  </div>
+  <h2>Stats</h2>
+  <table class="leaderboard">
+    <tr class="leaderboard-row leaderboard-header">
+      <th class="leaderboard-rank">Rank</th>
+      <th class="leaderboard-rating">Rating</th>
+      <th class="leaderboard-bot">Bot</th>
+      <th class="leaderboard-author">Author</th>
+      <th>Winrate</th>
+      <th>Matches</th>
+    </tr>
+    {#each leaderboard as entry, index}
+      <tr class="leaderboard-row">
+        <td class="leaderboard-rank">{index + 1}</td>
+        <td class="leaderboard-rating">
+          {entry["rating"].toFixed(0)}
+        </td>
+        <td class="leaderboard-bot">
+          <a class="leaderboard-href" href="/bots/{entry['bot']['name']}"
+            >{entry["bot"]["name"]}
+          </a></td
+        >
+        <td class="leaderboard-author">
+          {#if entry["author"]}
+            <!-- TODO: remove duplication -->
+            <a class="leaderboard-href" href="/users/{entry['author']['username']}"
+              >{entry["author"]["username"]}</a
+            >
+          {/if}
+        </td>
+        {#if mergedStats[entry["bot"]["name"]]}
+          <td>
+            {winRate(mergedStats[entry["bot"]["name"]]).toFixed(2)}
+          </td>
+          <td>
+            <a href={`/matches?bot=${bot["name"]}&opponent=${entry["bot"]["name"]}`}>view matches</a
+            >
+          </td>
+        {:else}
+          <td />
+          <td>no matches yet </td>{/if}
+      </tr>
+    {/each}
+  </table>
+</div>
+
+<style lang="scss">
+  .container {
+    width: 800px;
+    max-width: 80%;
+    margin: 50px auto;
+  }
+
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+    margin-bottom: 60px;
+    border-bottom: 1px solid black;
+  }
+
+  $header-space-above-line: 12px;
+
+  .bot-name {
+    font-size: 24pt;
+    margin-bottom: $header-space-above-line;
+  }
+
+  .owner-name {
+    font-size: 14pt;
+    text-decoration: none;
+    color: #333;
+    margin-bottom: $header-space-above-line;
+  }
+
+  .leaderboard {
+    margin: 18px 10px;
+    text-align: center;
+  }
+
+  .leaderboard th,
+  .leaderboard td {
+    padding: 8px 16px;
+  }
+  .leaderboard-rank {
+    color: #333;
+  }
+
+  .leaderboard-href {
+    text-decoration: none;
+    color: black;
+  }
+</style>
-- 
cgit v1.2.3

